#!/usr/bin/python3
# -*- coding: utf-8 -*-

import os
import sys
import json
import xml2dict
import datetime

from PyQt5.QtCore import Qt, QPoint, QRect, QTimer
from PyQt5.QtGui import QPainter, QPalette, QImage, QFont, QPaintEvent, QColor, QPen
from PyQt5.QtWidgets import QWidget

from database_setting import DatabaseSetting
from data_agent import DataAgent


############################################################
#                 Base and General View                    #
############################################################
class BaseView(QWidget):
    COLOR_MAP = {
            'white'     : Qt.white,
            'black'     : Qt.black,
            'red'       : Qt.red,
            'green'     : Qt.green,
            'blue'      : Qt.blue,
            'yellow'    : Qt.yellow,
            'magenta'   : Qt.magenta,
            'cyan'      : Qt.cyan,
            'gray'      : Qt.gray,
            'darkgray'  : Qt.darkGray,
            'darkred'   : Qt.darkRed,
            'darkgreen' : Qt.darkGreen,
            'darkblue'  : Qt.darkBlue,
            }

    def parseColor(color: str):
        if color in BaseView.COLOR_MAP:
            return BaseView.COLOR_MAP[color]
        elif color[0] == '#' and len(color) == 7:
            # parse #RRGGBB string
            r_str = color[1:3]
            r = int(r_str, 16)
            g_str = color[3:5]
            g = int(g_str, 16)
            b_str = color[5:7]
            b = int(b_str, 16)
            return QColor(r, g, b)
        else:
            print('unrecognized color', color)
            raise

    def __init__(self, parent=None):
        super(BaseView, self).__init__(parent)
        self.name = None

        # Data settings
        self.point_name = None
        self.da = None
        self.period = None
        self.value = None
        self.prev_value = None

        # GUI settings
        self.lines = []
        self.views = []
        self.h_align = Qt.AlignHCenter
        self.v_align = Qt.AlignVCenter
        self.border = True
        self.font = None
        self.font_family = None
        self.font_size = None
        self.font_style = None

        # QtWidget.setParent() method
        # parent widget will redraw all children widget in update()
        self.setParent(parent)

        self.parent = parent
        # inherit parent's attributes
        if parent is not None:
            self.inheritDataAgent(parent)
            self.inheritGUISetting(parent)

        if self.font is not None:
            self.font_family = self.font.family()
            self.font_size = self.font.pointSize()
            self.font_style = self.font.style()

    def inheritGUISetting(self, parent):
        self.border = parent.border
        self.font = parent.font
        self.front_color = parent.front_color
        self.bg_color = parent.bg_color

    def inheritDataAgent(self, parent):
        if parent is not None:
            self.setDataAgent(parent.da)

    def setDataAgent(self, da: DataAgent):
        self.da = da

    # register the points' names to DataAgent,
    # then DataAgent refreshes the data in each period
    def bindPoint(self, point_list: list):
        """
        This function registers the points' names to DataAgent
         so that the Agent refreshes their value periodically.
        :param point_list: point names in a list
        :return: N/A
        """
        if self.da is None:
            print(self.name, 'should do setDataAgent() before bindPoint()!')
            raise

        for item in point_list:
            self.da.local_register(item)

    # a view has only one point by default
    # override this function if the view has a lot of points to be updated
    def collectData(self):
        """
        This function collect the data of the view.
        Generally, a view bound only one point saved in self.value.
        This function should be overwritten when more points are bound.
        :return: N/A
        """
        if self.da is None:
            print(self.name, 'should do setDataAgent() before collectData()!')
            raise

        # get data from DataAgent
        if self.point_name is not None:
            self.value = self.da.local_read(self.point_name)

    def compute(self):
        """
        This function is used to do computation for values of points.
        It is left empty intended here.
        Derived class/instance overwrites this function.
        :return: N/A
        """
        pass

    def checkChange(self):
        """
        This function is used to confirm that
        the new value and previous one are different.
        If they are same, no GUI update is needed.
        :return: whether a GUI update is needed.
        """
        if self.value != self.prev_value:  # value changes
            if self.value is not None:
                self.prev_value = self.value
                return True
            else:  # view keeps original value
                return False
        else:
            return False

    def updateData(self):
        self.collectData()
        self.compute()
        return self.checkChange()

    # update() => repaint() => paintEvent()
    def updateUI(self):
        self.update()  # redraw itself
        return True

    def updateView(self):
        """
        This function updates View's data.
        And it updates View's GUI if data is changed.
        Then it enumerates and updates all subviews.
        :return:
        """
        if self.updateData():
            self.updateUI()

        if len(self.views) > 0:
            for view in self.views:
                result = view.updateView()

    def _drawBorder(self, painter: QPainter):
        if self.border:
            painter.save()

            painter.setPen(Qt.red)
            width = self.width()
            height = self.height()
            painter.drawRect(0, 0, width-1, height-1)

            painter.restore()

    def _drawLine(self, painter: QPainter):
        painter.save()

        painter.setPen(self.front_color)
        for line in self.lines:
            painter.drawLine(line[0], line[1])

        painter.restore()

    def _drawText(self, painter: QPainter, text: str):
        painter.save()

        painter.setPen(self.front_color)
        painter.setFont(self.font)
        width = self.width()
        height = self.height()
        rect = QRect(0, 0, width, height)
        painter.drawText(rect, self.h_align | self.v_align, text)

        painter.restore()

    def _drawImage(self,
                   painter: QPainter,
                   image: QImage,
                   extended: bool = True):
        painter.save()

        if extended:
            rect = QRect(0, 0, self.width(), self.height())
            painter.drawImage(rect, image)
        else:
            # compute left/top corner for AlignCenter
            width = image.width()
            height = image.height()
            x = (self.width() - width) / 2
            y = (self.height() - height) / 2
            painter.drawImage(QPoint(x, y), image)

        painter.restore()

    def paintEvent(self, event: QPaintEvent):
        painter = QPainter()
        painter.begin(self)

        if len(self.lines) > 0:
            self._drawSplitLine(painter)

        self._drawBorder(painter)

        painter.end()

    def parseLine(self, line_attr: dict):
        x1 = y1 = x = y = None

        if 'x' in line_attr:
            x = int(line_attr['x'])
        if '@x' in line_attr:
            x = int(line_attr['@x'])
        if x is None:
            print(line_attr, 'Line sentence has no "x"!')
            raise

        if 'y' in line_attr:
            y = int(line_attr['y'])
        if '@y' in line_attr:
            y = int(line_attr['@y'])
        if y is None:
            print(line_attr, 'Line sentence has no "y"!')
            raise

        if 'x1' in line_attr:
            x1 = int(line_attr['x1'])
        if '@x1' in line_attr:
            x1 = int(line_attr['@x1'])
        if x1 is None:
            print(line_attr, 'Line sentence has no "x1"!')
            raise

        if 'y1' in line_attr:
            y1 = int(line_attr['y1'])
        if '@y1' in line_attr:
            y1 = int(line_attr['@y1'])
        if y1 is None:
            print(line_attr, 'Line sentence has no "y1"!')
            raise

        self.lines.append((QPoint(x, y), QPoint(x1, y1)))

    def parseSplitLine(self, splitline_attr: dict):
        # TODO parse color and width

        if 'Line' in splitline_attr:
            line = splitline_attr['Line']
            if isinstance(line, list):  # lines
                for one_line in line:
                    self.parseLine(one_line)
            else:  # only one line
                self.parseLine(line)

    # attribute settings replace the member inherited or set by default
    def parseAttribute(self, attrs: dict):
#       BaseView.dumpAttribute(view_attr)

        name = None
        if 'name' in attrs:
            name = attrs['name']
        if '@name' in attrs:
            name = attrs['@name']
        if name is not None:
            self.name = name

        border = None
        if 'border' in attrs:
            border = attrs['border']
        if '@border' in attrs:
            border = attrs['@border']
        if border is not None:
            if border == 'true' or border == 'True':
                self.border = True
            elif border == 'false' or border == 'False':
                self.border = False

#       print('\tparsing', self.name, 'attributes ...')

        if 'Point' in attrs:
            self.parsePoint(attrs['Point'])
        if '@Point' in attrs:
            self.parsePoint(attrs['@Point'])

        if 'period' in attrs:
            self.period = attrs['period']
        if '@period' in attrs:
            self.period = attrs['@period']
        if self.period is not None:
            self.period = int(self.period)
#       print(self.name, 'period is', self.period)

        # if the view does not have a period, it inherits parent's period.
        # otherwise, the view should create an individual thread with its
        # period to update data

        x = None
        # parse and reset position
        if 'x' in attrs:
            x = int(attrs['x'])
        if '@x' in attrs:
            x = int(attrs['@x'])
        y = None
        if 'y' in attrs:
            y = int(attrs['y'])
        if '@y' in attrs:
            y = int(attrs['@y'])
        if x is None or y is None:
            print(self.name, 'position x or y is absent!')
            raise
        self.move(x, y)

        # parse and reset size
        width = None
        if 'width' in attrs:
            width = int(attrs['width'])
        if '@width' in attrs:
            width = int(attrs['@width'])
        height = None
        if 'height' in attrs:
            height = int(attrs['height'])
        if '@height' in attrs:
            height = int(attrs['@height'])
        if width is None or height is None:
            print(self.name, 'width or height is absent!')
            raise
        self.resize(width, height)

        # parse and reset font
        if 'font_name' in attrs:
            self.font_family = attrs['font_name']
        if '@font_name' in attrs:
            self.font_family = attrs['@font_name']

        if 'font_size' in attrs:
            self.font_size = int(attrs['font_size'])
        if '@font_size' in attrs:
            self.font_size = int(attrs['@font_size'])

        if '@font_style' in attrs:
            font_style = attrs['@font_style']
        elif 'font_style' in attrs:
            font_style = attrs['font_style']
        else:
            font_style = 'normal'

        if 'bold' == font_style:
            self.font_style = QFont.Bold
        elif 'italic' == font_style:
            self.font_style = QFont.Italic
        else:
            self.font_style = QFont.Normal

        if self.font is not None:
            if self.font_family is None:
                self.font_family = self.font.family()
            if self.font_size is None:
                self.font_size = self.font.pointSize()
            if self.font_style is None:
                self.font_style = self.font.style()

        self.font = QFont(self.font_family, self.font_size, self.font_style)

        h_align = None
        if 'h_align' in attrs:
            h_align = attrs['h_align']
        if '@h_align' in attrs:
            h_align = attrs['@h_align']

        if h_align is not None:
            if h_align == 'left':
                self.h_align = Qt.AlignLeft
            elif h_align == 'right':
                self.h_align = Qt.AlignRight
            elif h_align == 'center':
                self.h_align = Qt.AlignHCenter
            else:
                print(self.name, 'has unsupport alignment', h_align)
                raise

        v_align = None
        if 'v_align' in attrs:
            v_align = attrs['v_align']
        if '@v_align' in attrs:
            v_align = attrs['@v_align']

        if v_align is not None:
            if v_align == 'top':
                self.v_align = Qt.AlignTop
            elif v_align == 'bottom':
                self.v_align = Qt.AlignBottom
            elif v_align == 'center':
                self.v_align = Qt.AlignVCenter
            else:
                print(self.name, 'has unsupport alignment', v_align)
                raise

        front_color = None
        if 'front_color' in attrs:
            front_color = attrs['front_color']
        if '@front_color' in attrs:
            front_color = attrs['@front_color']

        if front_color is not None:
            self.front_color = BaseView.parseColor(front_color)

        bg_color = None
        if 'bg_color' in attrs:
            bg_color = attrs['bg_color']
        if '@bg_color' in attrs:
            bg_color = attrs['@bg_color']
        if bg_color is not None:
            self.bg_color = BaseView.parseColor(bg_color)
            self.setBgColor(self.bg_color)

        if 'SplitLine' in attrs:
            self.parseSplitLine(attrs['SplitLine'])

    def dumpAttribute(attrs: dict):
        print(json.dumps(attrs, indent=4))

    def setBgColor(self, color: QColor):
        self.setPalette(QPalette(color))
        self.setAutoFillBackground(True)

    # parse and bind point
    def parsePoint(self, point_attrs: dict):
        if isinstance(point_attrs, list):
            point_list = []
            for point_item in point_attrs:
                if 'name' in point_item:
                    point_list.append(point_item['name'])
                elif '@name' in point_item:
                    point_list.append(point_item['@name'])
                else:
                    print(self.name, 'invalid point item', point_item)
                    raise

            self.bindPoint(point_list)

        elif isinstance(point_attrs, str):
            # for single point
            self.point_name = point_attrs
            self.bindPoint([self.point_name])

        else:
            print(self.name, 'has unrecognized point', point_attrs)
            raise


class ReservedView(BaseView):
    DEFAULT_FRONT_COLOR = Qt.magenta
    DEFAULT_BG_COLOR = Qt.cyan

    def __init__(self, parent):
        super(ReservedView, self).__init__(parent)

    def parseAttribute(self, attrs: dict):
        BaseView.parseAttribute(self, attrs)

        width = self.width()
        height = self.height()
        self.lines.append((QPoint(0, 0), QPoint(width-1, height-1)))
        self.lines.append((QPoint(width-1, 0), QPoint(0, height-1)))

        # replace the front and background color
        self.front_color = ReservedView.DEFAULT_FRONT_COLOR
        self.bg_color = ReservedView.DEFAULT_BG_COLOR
        self.setBgColor(self.bg_color)

    def updateData(self):
        # an intended empty function
        return False

    def updateView(self):
        # an intended empty function
        pass

    # override BaseView paintEvent(event)
    def paintEvent(self, event: QPaintEvent):
        painter = QPainter()
        painter.begin(self)

        self._drawLine(painter)
        self._drawText(painter, self.name)
        self._drawBorder(painter)

        painter.end()


class StaticTextView(BaseView):
    def __init__(self, parent=None):
        super(StaticTextView, self).__init__(parent)
        self.text = 'StaticText'

    def parseAttribute(self, attrs: dict):
        BaseView.parseAttribute(self, attrs)

        if 'text' in attrs:
            self.text = attrs['text']
        if '@text' in attrs:
            self.text = attrs['@text']

    def updateData(self):
        """
        A StaticTextView has no point to be updated.
        :return: always False
        """
        return False

    def paintEvent(self, event: QPaintEvent):
        painter = QPainter()
        painter.begin(self)

        self._drawText(painter, self.text)
        self._drawBorder(painter)

        painter.end()


class RawTextView(BaseView):
    def __init__(self, parent=None):
        super(RawTextView, self).__init__(parent)
        self.text = self.value = 'DynRawText'
        self.value_type = 'int'

    def parseAttribute(self, attrs: dict):
        BaseView.parseAttribute(self, attrs)

        if 'value_type' in attrs:
            self.value_type = attrs['value_type']
        if '@value_type' in attrs:
            self.value_type = attrs['@value_type']

    def updateData(self):
        redraw_flag = BaseView.updateData(self)
        if redraw_flag:
            if self.value_type == 'int':
                self.text = str(round(float(self.value)))
            elif self.value_type == 'float':
                self.text = str(self.value)
            elif self.value_type == 'string':
                self.text = str(self.value)
            else: # Unsupported type
                raise

        return redraw_flag

    def paintEvent(self, event: QPaintEvent):
        painter = QPainter()
        painter.begin(self)

        self._drawText(painter, self.text)
        self._drawBorder(painter)

        painter.end()


class TextView(RawTextView):
    def __init__(self, parent=None):
        super(TextView, self).__init__(parent)
        self.text_map = {}

    def parseTextItem(self, item: dict):
        value = None
        text = None

        if 'value' in item:
            value = item['value']
        if '@value' in item:
            value = item['@value']
        if value is None:
            print(self.name, 'has no "value" in TextMapping sentence', item)
            raise

        if 'text' in item:
            text = item['text']
        if '@text' in item:
            text = item['@text']
        if text is None:
            print(self.name, 'has no "text" in TextMapping sentence', item)
            raise

        self.text_map[value] = text

    def parseTextMapping(self, attrs: dict):
        if isinstance(attrs, list): # many items
            for item in attrs:
                self.parseTextItem(item)
        else: # only one item
            self.parseTextItem(attrs)

    def parseAttribute(self, attrs: dict):
        BaseView.parseAttribute(self, attrs)

        if 'TextMapping' in attrs:
            self.parseTextMapping(attrs['TextMapping'])
        else:
            print(self.name, 'has no TextMapping sentence')
            raise

    def updateData(self):
        redraw_flag = RawTextView.updateData(self)

        if redraw_flag and self.value is not None:
            if self.value in self.text_map:
                self.text = self.text_map[self.value]
            else:
                print(self.name, 'has unmapped value', self.value)
                raise

        return redraw_flag


class AlarmTextView(RawTextView):
#     # TODO: extend:
#     #   low_limit/low2limit/low3limit
#     #   high_limit/high2limit/high3limit
#     def parsePoint(self, point_attrs: dict):
#         if isinstance(point_attrs, list):
#             point_list = []
#             for point_item in point_attrs:
#                 if 'name' in point_item:
#                     point_list.append(point_item['name'])
#                 elif '@name' in point_item:
#                     point_list.append(point_item['@name'])
#                 # extend low_limit/low2limit/low3limit
#                 elif 'low_limit' in point_item:
#                     self.low_limit = point_item['low_limit']
#                     point_list.append()
#                 elif '@low_limit' in point_item:
#                     point_list.append(point_item['@low_limit'])
#                 # extend high_limit/high2limit/high3limit
#                     point_list.append(point_item['@low3limit'])
#                 elif 'high_limit' in point_item:
#                     point_list.append(point_item['high_limit'])
#                 elif '@high_limit' in point_item:
#                     point_list.append(point_item['@high_limit'])
#                 else:
#                     print(self.name, 'invalid point item', point_item)
#                     raise
#             self.bindPoint(point_list)
#         else:
#             print(self.name, 'AlarmView has no limit point!', point_attrs)
#             raise

    # override:
    #   collectData()
    #   compute()
    #   checkChange()
    def updateData(self):
        # collectData
        RawTextView.collectData(self)

        # TODO: get all points' value

        # compute

        # checkChange


class StaticImageView(BaseView):
    def __init__(self, parent=None):
        super(StaticImageView, self).__init__(parent)
        self.image = None
        self.extended = True

    def parseImageDirectory(self, attrs: dict):
        image_dir = None

        if 'image_dir' in attrs:
            image_dir = attrs['image_dir']
        if '@image_dir' in attrs:
            image_dir = attrs['@image_dir']

        if image_dir is None: # no directory is specified
            self.image_dir = '.' # use current directory
        else:
            if os.path.exists(image_dir):
                self.image_dir = image_dir
            else:
                print(self.name, 'access', image_dir, 'denied!')
                raise

        return self.image_dir

    def loadImage(self, image_path: str):
        if os.path.exists(image_path):
            print(self.name, 'load image file:', image_path)
            return QImage(image_path)
        else:
            print(self.name, 'access', image_path, 'denied!')
            raise

    def parseExtended(self, attrs: dict):
        # The image will be stretched when extended flag is set.
        flag = None

        if 'extended' in attrs:
            flag = attrs['extended']
        if '@extended' in attrs:
            flag = attrs['@extended']

        if flag is not None:
            if flag == 'false' or flag == 'False':
                self.extended = False
            elif flag == 'true' or flag == 'True':
                self.extended = True
            else:
                print(self.name, 'unsupported extended type', flag)

        return self.extended

    def parseAttribute(self, attrs: dict):
        BaseView.parseAttribute(self, attrs)
        self.parseImageDirectory(attrs)
        self.parseExtended(attrs)

        if 'image' in attrs:
            image_path = os.path.join(self.image_dir, attrs['image'])
            self.loadImage(image_path)
        else:
            print(self.name, 'has no image_file!')
            raise

    def updateData(self):
        """
        A StaticImageView has no point to be updated.
        :return: always False
        """
        return False

    def paintEvent(self, event: QPaintEvent):
        painter = QPainter()
        painter.begin(self)

        if self.image is not None:
            self._drawImage(painter, self.image, self.extended)

        self._drawBorder(painter)

        painter.end()


class ImageView(StaticImageView):
    def __init__(self, parent=None):
        super(ImageView, self).__init__(parent)
        self.image_map = {}

    def parseImageItem(self, item: dict):
        value = None
        image = None
        image_name = None

        if 'value' in item:
            value = item['value']
        if '@value' in item:
            value = item['@value']
        if value is None:
            print(self.name, 'has no "value" in ImageMapping sentence', item)
            raise

        if 'image' in item:
            image_name = item['image']
        if '@image' in item:
            image_name = item['@image']
        if image_name is None:
            print(self.name, 'has no "image" in ImageMapping sentence', item)
            raise

        image_path = os.path.join(self.image_dir, image_name)
        image = self.loadImage(image_path)
        self.image_map[value] = image

    def parseImageMapping(self, attrs):
        if isinstance(attrs, list):  # many items
            for item in attrs:
                self.parseImageItem(item)
        else: # only one item
            self.parseImageItem(attrs)

    def parseAttribute(self, attrs: dict):
        BaseView.parseAttribute(self, attrs)
        self.parseImageDirectory(attrs)
        self.parseExtended(attrs)

        if 'ImageMapping' in attrs:
            self.parseImageMapping(attrs['ImageMapping'])
        else:
            print(self.name, 'has no ImageMapping sentence')
            raise

    def updateData(self):
        redraw_flag = BaseView.updateData(self)

        if redraw_flag:
            if self.value in self.image_map:
                self.image = self.image_map[self.value]
            else:
                print(self.name, 'has unmapped value', self.value)
                raise

        return redraw_flag

############################################################
#                       Root View                          #
############################################################
class RootView(BaseView):
    DEFAULT_PERIOD = 1000  # 1 second

    DEFAULT_FRONT_COLOR = Qt.magenta
    DEFAULT_BG_COLOR = Qt.black

    def __init__(self):
        super(RootView, self).__init__(parent=None)
        self.name = 'RootView'

        self.front_color = RootView.DEFAULT_FRONT_COLOR
        self.bg_color = RootView.DEFAULT_BG_COLOR

        self.period = RootView.DEFAULT_PERIOD

    def closeEvent(self, event):
        """
        This function block closeEvent() to terminate/clean all subscribe
        threads with DataAgent.remove_all_sub().
        :param event:
        :return:
        """
        if self.da is not None:
            self.da.remove_all_sub()

    def parseAttribute(self, attrs: dict):
        """
        This function parses RootView and its subviews' attributes.
        A timer is created for update GUI when parsing is done.
        :param attrs: attributes of a RootView
        :return: N/A
        """
        attrs = attrs['RootView']

        if 'DatabaseSetting' in attrs:
            db_attrs = attrs['DatabaseSetting']
            self.db = DatabaseSetting()
            self.db.parse_db(db_attrs)
            da = DataAgent(self.db.ip, self.db.port)
            self.setDataAgent(da)

        # parse my attributes
        BaseView.parseAttribute(self, attrs)

        # parse subviews
        for key in attrs.keys():
            # a View instance's name must end with 'View'
            if key.endswith('View'):
                view_class_name = key
                subview = attrs[view_class_name]
                if isinstance(subview, list):
                    for view_item in subview:
                        ViewFactory.createViewByName(self,
                                                     view_class_name,
                                                     view_item)
                else:  # only one instance for this view type
                    ViewFactory.createViewByName(self,
                                                 view_class_name,
                                                 subview)

        # create a timer to call self.updateView() periodically
        self.timer = QTimer(self)
        self.timer.timeout.connect(self.updateView)
        self.timer.start(self.period)

    def parseJson(self, f):
        """
        This function is used to parse a GUI setting content in Json format.
        :param f: GUI file object with Json content
        :return: attributes in the setting
        """
        attrs = json.load(f)
#       print(attrs)
        return attrs

    def parseXml(self, f):
        """
        This function is used to parse a GUI setting content in XML format.
        :param f: GUI file object with XML content
        :return: attributes in the setting
        """
        content = f.read()
        attrs = xml2dict.parse(content)
#       print(attrs)
        return attrs

    def parseSettingFile(self, filepath: str):
        """
        This function is used to parse a GUI setting.
        :param filepath: GUI setting file path
        :return: N/A
        """
        attrs = None

        with open(filepath, 'r') as f:
            if filepath.endswith('.xml'):
                attrs = self.parseXml(f)
            elif filepath.endswith('.json'):
                attrs = self.parseJson(f)
            else:
                print('Unsupported file type!')
                sys.exit()
#       print(json.dumps(attrs, indent=4))
        self.parseAttribute(attrs)

    def updateData(self):
        """
        This function overwrites BaseView.updateData().
        The RootView DOES NOT bind any point!
        So it is just ask DataAgent to pull all registered points to local.
        It is called by updateView() implicitly here.
        :return: True if at least one point is gotten. Otherwise, return False.
        """
        if self.da is None:
            print('Access DataAgent failure!')
            return False

#       print(datetime.datetime.now(), self.name, 'is updating data')
        result = self.da.pull_to_local()
        if len(result) > 0:
            return True
        else:
            return False

    def paintEvent(self, event: QPaintEvent):
        """
        This function overwrites BaseView.paintEvent().
        It just draws splitlines described in the setting file.
        :param event: QPaintEvent
        :return: N/A
        """
        painter = QPainter()
        painter.begin(self)

        if len(self.lines) > 0:  # draw splitline
            self._drawLine(painter)

        painter.end()


############################################################
#                      View Factory                        #
############################################################
class ViewFactory():
    """
    Create a view instance when its name is recognized.
    If the view is not recognized, return None.
    Otherwise, dump the flawed attributes.
    :param parent_view: direct parent in View hierarchy
    :param view_class_name: class name of the view, ends with 'view'
    :param view_attr: view's attributes to be parsed after it is created
    :returns: the view instance if success.
    """
    def createViewByName(parent_view: BaseView,
                         view_class_name: str,
                         view_attr: dict):
        view = eval(view_class_name)(parent_view)

        if view is not None:
            print('Create a', view_class_name, 'instance success.')
            if view is not None:
                parent_view.views.append(view)
                view.parseAttribute(view_attr)
                if view.name is not None:
                    print('\tNew', view_class_name, 'name is', view.name)
                else:
                    print('\tUnnamed view:')
                    BaseView.dumpAttribute(view_attr)
        else:
            print('Create', view_class_name, 'instance failed!')

        return view


############################################################
#                       Misc Views                         #
############################################################
class DateView(RawTextView):
    def __init__(self, parent=None):
        super(DateView, self).__init__(parent)
        self.text = self.value = 'DateText'
        self.value_type = 'string'

    def updateData(self):
        self.text = datetime.datetime.now().strftime('%Y-%m-%d')
        return True


class TimeView(RawTextView):
    def __init__(self, parent=None):
        super(TimeView, self).__init__(parent)
        self.text = self.value = 'TimeText'
        self.value_type = 'string'

    def updateData(self):
        self.text = datetime.datetime.now().strftime('%H:%M:%S')
        return True


class BatteryIconView(ImageView):
    def __init__(self, parent=None):
        super(BatteryIconView, self).__init__(parent)
        self.prev_index = None

    def updateData(self):
        redraw_flag = BaseView.updateData(self)

        index = 0
        if redraw_flag:
            value = int(self.value)
            if value > 85:
                index = "3"
            elif value > 30:
                index = "2"
            elif value > 10:
                index = "1"
            else:
                index = "0"

            self.image = self.image_map[index]
            if self.prev_index is None:
                self.prev_index = index
                return True
            elif self.prev_index != index:
                self.prev_index = index
                return True
            else:
                return False
        else:
            return False


class BatteryPercentView(RawTextView):
    def updateData(self):
        redraw_flag = BaseView.updateData(self)
        if redraw_flag:
            self.text = self.value + '%'

            # update front color
            value = int(self.value)
            if value < 10:
                self.front_color = Qt.red
            elif value < 30:
                self.front_color = Qt.yellow
            else:
                self.front_color = Qt.green


############################################################
#                      Medical Views                       #
############################################################
class HeartbeatView(RawTextView):

    # overwrite updateData() to disable collectData(), compute(), checkChange()
    # and set highlight color for speedy heartbeats for various patients
    def updateData(self):
        # collect data
        # > patient type
        patient_type = self.da.local_read('patient_type')
        if patient_type is None:
            print(self.name, 'unable to retrieve patient_type')
            raise
        # > heartbeat
        heartbeat = self.da.local_read('heartbeat')
        if heartbeat is None:
            print(self.name, 'unable to retrieve heartbeat')
            raise

        # compute
        self.value = round(float(heartbeat))

        # check value changes
        redraw_flag = False
        if self.value != self.prev_value:
            # adult
            if patient_type == '0.0' and self.value >= 120:
                self.front_color = Qt.red
            # baby
            elif patient_type == '1.0' and self.value >= 140:
                self.front_color = Qt.red
            # infant
            elif patient_type == '2.0' and self.value >= 160:
                self.front_color = Qt.red
            else:
                self.front_color = Qt.green

            # set redraw_flag and text
            redraw_flag = True
            self.text = str(self.value)

            # update previous value
            self.prev_value = self.value

        return redraw_flag


class TempratureView(RawTextView):

    # overwrite updateData() to disable collectData(), compute(), checkChange()
    # and set highlight color for high temprature alarm
    def updateData(self):
        # collect data of temprature
        temp = self.da.local_read('temprature')
        if temp is None:
            print(self.name, 'unable to retrieve temprature')
            raise

        # compute
        self.value = float(temp)

        # check value changes
        redraw_flag = False
        if self.value != self.prev_value:
            if self.value > 37.1:
                self.front_color = Qt.red
            else:
                self.front_color = Qt.gray

            # set redraw_flag and text
            redraw_flag = True
            self.text = str(self.value)

            # update previous value
            self.prev_value = self.value

        return redraw_flag


class PulseView(BaseView):
    VALUE_NUM = 480

    # a singleton for this view
    singleton = None

    def updateChange(msg):
        value = msg['data']

        pulse_view = PulseView.singleton
        pulse_view.updateData(value)
        pulse_view.update()  # force repaint with new value

    def __init__(self, parent=None):
        super(PulseView, self).__init__(parent)
        self.index = 0
        self.value = 0
        self.num = 0

        # history data array for redrawing all
        self.data = []
        self.points = []

        PulseView.singleton = self

    # replace/disable inherited functions()
    def collectData(self):
        pass

    def updateView(self):
        pass

    def parseAttribute(self, attrs: dict):
        BaseView.parseAttribute(self, attrs)

        # replace the front and background color
        self.front_color = Qt.green
        self.bg_color = Qt.black
        self.setBgColor(self.bg_color)

        # subscribe the value change of 'pulse' point
        self.da.subscribe('pulse', PulseView.updateChange)

    def updateData(self, value):
        if self.num < PulseView.VALUE_NUM:
            # extend array and store new data
            self.data.append(value)

            self.index = self.num

            value = int(float(value))
#           print(value)
            self.points.append(QPoint(self.index, value))

            self.num += 1

        else:
            # replace historical data
            self.data[self.index] = value

            value = int(float(value))
#           print(value)
            self.points[self.index] = QPoint(self.index, value)

            self.index = self.index + 1
            if self.index >= PulseView.VALUE_NUM:
                self.index = 0

        return True  # always return True to redraw

    def realDrawing(self, painter: QPainter):
        painter.save()

        pen = QPen()
        pen.setColor(Qt.green)
        pen.setStyle(Qt.SolidLine)
        pen.setWidth(2)

        painter.setPen(pen)

        for i in range(0, self.num-1):
            painter.drawLine(self.points[i], self.points[i+1])
            # or use drawMultipleLine to draw

        # draw the black gap
        black_pen = QPen()
        black_pen.setColor(Qt.black)
        black_pen.setStyle(Qt.SolidLine)
        painter.setPen(black_pen)
        painter.setBrush(QColor(Qt.black))
        x = self.index - 2
        y = self.height() - 1
        painter.drawRect(x, 0, 8, y)

        painter.restore()

    def stubDrawing(self, painter: QPainter):
        painter.save()

        pen = QPen()
        pen.setColor(Qt.green)
        pen.setStyle(Qt.SolidLine)
        pen.setWidth(2)

        painter.setPen(pen)
        # 1st period
        painter.drawLine(QPoint(  0,  60), QPoint( 40,  60))
        painter.drawLine(QPoint( 40,  60), QPoint( 50,  15))
        painter.drawLine(QPoint( 50,  15), QPoint( 60,  90))
        painter.drawLine(QPoint( 60,  90), QPoint( 70,  60))
        painter.drawLine(QPoint( 70,  60), QPoint( 99,  60))

        # 2nd period
        painter.drawLine(QPoint(100,  60), QPoint(140,  60))
        painter.drawLine(QPoint(140,  60), QPoint(150,  15))
        painter.drawLine(QPoint(150,  15), QPoint(160,  90))
        painter.drawLine(QPoint(160,  90), QPoint(170,  60))
        painter.drawLine(QPoint(170,  60), QPoint(199,  60))

        # 3rd period
        painter.drawLine(QPoint(200,  60), QPoint(240,  60))
        painter.drawLine(QPoint(240,  60), QPoint(250,  15))
        painter.drawLine(QPoint(250,  15), QPoint(260,  90))
        painter.drawLine(QPoint(260,  90), QPoint(270,  60))
        painter.drawLine(QPoint(270,  60), QPoint(299,  60))

        # 4th period
        painter.drawLine(QPoint(300,  60), QPoint(340,  60))
        painter.drawLine(QPoint(340,  60), QPoint(350,  15))
        painter.drawLine(QPoint(350,  15), QPoint(360,  90))
        painter.drawLine(QPoint(360,  90), QPoint(370,  60))
        painter.drawLine(QPoint(370,  60), QPoint(399,  60))

        # 5th period
        painter.drawLine(QPoint(400,  60), QPoint(440,  60))
        painter.drawLine(QPoint(440,  60), QPoint(450,  15))
        painter.drawLine(QPoint(450,  15), QPoint(460,  90))
        painter.drawLine(QPoint(460,  90), QPoint(470,  60))
        painter.drawLine(QPoint(470,  60), QPoint(480,  60))

        # draw the black gap
        black_pen = QPen()
        black_pen.setColor(Qt.black)
        black_pen.setStyle(Qt.SolidLine)
        painter.setPen(black_pen)
        painter.setBrush(QColor(Qt.black))
        x = self.index
        y = self.height()
        painter.drawRect(x+1, 0, 8, y-1)

        painter.restore()

    def paintEvent(self, event: QPaintEvent):
        painter = QPainter()
        painter.begin(self)

#       self.stubDrawing(painter)
        self.realDrawing(painter)

        painter.end()


############################################################
#                     Building Views                       #
############################################################
# Room Temperature use RawTextView
# Room Humidity use RawTextView
# Fire Alarm use AlarmTextView
